> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt
> Use this file to discover all available pages before exploring further.

# Shared terminal

> Real-time terminal synchronization powered by PTY and VT10x emulation

## Overview

Duet's shared terminal provides a fully synchronized shell environment where all participants see exactly the same output in real time. Every keystroke, command, and output is instantly visible to everyone in the session.

## How it works

The terminal is built on three key technologies:

<CardGroup cols={3}>
  <Card title="PTY" icon="rectangle-terminal">
    Pseudoterminal provides a real shell process with full terminal capabilities
  </Card>

  <Card title="VT10x" icon="code">
    Terminal emulator handles ANSI escape sequences, colors, and cursor positioning
  </Card>

  <Card title="Pub/Sub" icon="tower-broadcast">
    Subscriber pattern broadcasts updates to all connected clients
  </Card>
</CardGroup>

## Terminal initialization

When a room is created, Duet starts a shell in an isolated workspace:

```go theme={null}
func (t *Terminal) Start() error {
    shell := os.Getenv("SHELL")
    if shell == "" {
        shell = "/bin/sh"
    }

    t.cmd = exec.Command(shell)
    t.cmd.Dir = t.workDir  // Isolated per-room directory
    t.cmd.Env = append(os.Environ(),
        "TERM=xterm-256color",
    )

    t.ptmx, err = pty.StartWithSize(t.cmd, &pty.Winsize{
        Rows: uint16(t.height),
        Cols: uint16(t.width),
    })
}
```

<Info>
  Each room gets its own shell process running in `/app/workspaces/{workspace-name}`. This provides complete isolation between sessions.
</Info>

## Real-time synchronization

Duet uses a subscriber pattern to keep all participants in sync:

<Steps>
  <Step title="Input from any client">
    When you type in the terminal, your keystrokes are sent directly to the PTY:

    ```go theme={null}
    // Special keys are mapped to ANSI sequences
    switch key {
    case "enter":
        data = []byte("\r")
    case "up":
        data = []byte("\x1b[A")
    case "backspace":
        data = []byte{127}
    }

    m.terminal.Write(data)
    ```
  </Step>

  <Step title="PTY processes the input">
    The shell receives the input and generates output (stdout/stderr).
  </Step>

  <Step title="VT10x parses the output">
    A background read loop feeds PTY output into the VT10x emulator:

    ```go theme={null}
    func (t *Terminal) readLoop() {
        buf := make([]byte, 4096)
        for {
            n, err := t.ptmx.Read(buf)
            t.vt.Write(buf[:n])  // Parse ANSI sequences
            t.dirty = true
            t.broadcast()  // Notify all subscribers
        }
    }
    ```
  </Step>

  <Step title="All clients re-render">
    Each connected client receives a notification and renders the updated terminal state.
  </Step>
</Steps>

## Rendering optimization

Duet minimizes CPU usage with smart caching:

```go theme={null}
func (t *Terminal) Render() string {
    // Return cached render if not dirty
    if !t.dirty && t.lastRender != "" {
        return t.lastRender
    }

    // Render cells with run-length encoding for colors
    for y := 0; y < rows; y++ {
        for x := range cols {
            cell := t.vt.Cell(x, y)
            // Only emit ANSI codes when colors change
            if cell.FG != prevFG {
                sb.WriteString(fgColor(cell.FG))
            }
            sb.WriteRune(cell.Char)
        }
    }

    t.lastRender = sb.String()
    t.dirty = false
    return t.lastRender
}
```

<Tabs>
  <Tab title="Performance">
    * **Caching**: Only re-renders when PTY output changes (dirty flag)
    * **Run-length encoding**: ANSI color codes only emitted when colors change
    * **Efficient broadcast**: Non-blocking channel sends to all subscribers
  </Tab>

  <Tab title="Color support">
    The terminal supports full 256-color mode:

    ```go theme={null}
    "TERM=xterm-256color"
    ```

    Colors 0-7: Standard ANSI\
    Colors 8-15: Bright variants\
    Colors 16-255: Extended palette
  </Tab>
</Tabs>

## Window resizing

When your terminal window changes size, Duet automatically adjusts:

```go theme={null}
func (t *Terminal) Resize(width, height int) {
    t.width = width
    t.height = height
    t.dirty = true
    t.lastRender = ""

    if t.vt != nil {
        t.vt.Resize(width, height)
    }

    if t.ptmx != nil {
        pty.Setsize(t.ptmx, &pty.Winsize{
            Rows: uint16(height),
            Cols: uint16(width),
        })
    }
}
```

<Warning>
  All participants share the same terminal size. If one client resizes their window, it affects everyone. The terminal dimensions are set when the room is created based on the host's window size.
</Warning>

## Typing indicators

Duet shows when other participants are actively typing:

```go theme={null}
if m.currentRoom != nil && time.Since(m.typingTime) > 500*time.Millisecond {
    m.currentRoom.BroadcastEvent(room.RoomEvent{
        Type:     "typing",
        Username: m.username,
    }, m.clientID)
    m.typingTime = time.Now()
}
```

Typing events are debounced to 500ms to avoid flooding the network. Indicators disappear after 2 seconds of inactivity.

## Cursor rendering

The cursor position is synchronized across all clients using reverse video:

```go theme={null}
isCursor := cursorVisible && x == cursor.X && y == cursor.Y

if isCursor {
    // Swap foreground/background for cursor
    fg, bg = bg, fg
}
```

Everyone sees the cursor blinking at the same position, making it clear who's actively working.

## Terminal lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> Created: New room
    Created --> Running: Start() called
    Running --> Running: Read/Write/Resize
    Running --> Closed: Last client leaves
    Closed --> [*]: Room destroyed
    
    note right of Running
        - PTY read loop active
        - VT10x parsing output
        - Broadcasting to subscribers
    end note
```

## Supported features

<AccordionGroup>
  <Accordion title="Full shell interaction">
    * Execute any shell command
    * Run interactive programs (vim, htop, etc.)
    * Tab completion
    * Command history (arrow keys)
    * Job control (Ctrl+C, Ctrl+Z)
  </Accordion>

  <Accordion title="ANSI escape sequences">
    * Cursor movement (\x1b\[A, \x1b\[B, etc.)
    * Text styling (bold, italic, underline)
    * 256-color palette
    * Clear screen / line
    * Save/restore cursor position
  </Accordion>

  <Accordion title="Special keys">
    All keyboard input is properly mapped:

    * Arrow keys → `\x1b[A` through `\x1b[D`
    * Home/End → `\x1b[H` / `\x1b[F`
    * Backspace → ASCII 127
    * Delete → `\x1b[3~`
    * Tab → `\t`
    * Enter → `\r`
  </Accordion>
</AccordionGroup>

## Workspace isolation

Each room operates in its own directory:

```go theme={null}
baseDir := "/app/workspaces"
workspaceDir := filepath.Join(baseDir, workspaceName)

// Copy template with basic tools
cmd := exec.Command("cp", "-r", "/app/workspace-template/.", workspaceDir)
```

<Note>
  Workspaces are completely isolated. You can create files, install packages, and run processes without affecting other rooms. Everything is cleaned up when the session ends.
</Note>

## Limitations

<Warning>
  The following are not currently supported:

  * Individual cursor positions per user
  * Different terminal sizes per participant
  * Screen splitting or pane management
  * Terminal scrollback history

  All participants share a single unified view of the terminal.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="AI assistant" icon="sparkles" href="/features/ai-assistant">
    Get AI-powered help while coding in the shared terminal
  </Card>

  <Card title="Sandbox execution" icon="cube" href="/features/sandbox-execution">
    Run isolated commands using Cloudflare Sandboxes
  </Card>
</CardGroup>
